iT邦幫忙

2022 iThome 鐵人賽

DAY 9
2
Web 3

從以太坊白皮書理解 web 3 概念系列 第 10

從以太坊白皮書理解 web 3 概念 - Day9

  • 分享至 

  • xImage
  •  

從以太坊白皮書理解 web 3 概念 - Day9

從今天開始到 Day 22 共 13 天

將會以 CryptoZombie 這個 solidity 學習網站

來了解 Solidity 這個語言

學完之後將能夠知道如何透過 Solidity 來開發 Small Contract

Solidity 簡介

Solidity 語言是 EVM 上能夠運行的一種腳本語言,用來撰寫 Small Contract

在這 13 天的介紹中

將分為成以下大章節:

Smart Contract 基礎資料結構與 Web3.js 互動介面:Day 9 - Day 13

Smart Contract 與去中心化 Oracle 互動: Day 14

Smart Contract 測試與如何建立 Oracle: Day 15 - Day 19

Smart Contract 周邊系統: Day 20 - Day 21

Learn Solidity - day 1 建立 ZombieContract

可以開啟另一個視窗跟著本文一起去實作

課程連結如下:

Lession 1: ZombieFactory

在開始撰寫 Contract 之前

先理解要實作的需求

ZombieContract 需求

ZombieContract 是一個生成 Zombie 物件的工廠

每個 Zombie 具有 name 與 dna 屬性

並且具有一個識別子 id 作為一個辨識生成過的編號

name 是一個隨機字串

dna 是由 id 與 name 做雜湊產生出來的字串

根據這些參數每個 Zombie 物件有都是有他獨特的樣貌

Contract 基礎要件

Solidity 編譯版本號

在每個 Contract 的代碼前都必須要標注編譯的 Solidity 版本號

因為每個版本都有其特別的功能及不同的資料結構支援

舉例來說:

要撰寫一個由 solidity 版本於 0.5.0 及 0.6.0 之間的 Contract

必須在上方宣告如下:

pramga solidity >=0.5.0 <0.6.0;

Contract 本體

Contract 本體如同物件導向語言裏面,對於物件會宣告一個建立物件的類別

當作建立物件的藍本。

Contract 也是一樣,會宣告一個開頭為 contract 帶有名稱的腳本區段

當作建立該 Contract 的腳本。

如下:

建立一個 ZombieFactory 的空腳本

pramga solidity >=0.5.0 <0.6.0;

contract ZombieFactory {}

狀態變數與整數

在 solidity 裏面,當在 contract 內部宣告一個變數時

這個變數將會永遠寫入 constract 的儲存空間內。代表狀態變數會被寫入區塊鏈上。

舉例來說:

contract Example {
    // myUnsignedInteger 會變成 Contract 內容寫入鏈上
    uint myUnsignedInteger = 100;
}

Unsigned Integers

在 solidity , uint 代表這個整數沒有負號,只能存正數。

另外,uint 代表 uint256 的別名。
其他 bit 位數的無號數還有 uint8, uint16, uint32 等等。

在 ZombieFactory 內,需要一個無號數來存儲 dnaDigits 並且設定為 16。

到此 ZombieFactory 如下

pramga solidity >=0.5.0 <0.6.0;

contract ZombieFactory {
    uint dnaDigits = 16;
}

數學運算符號

在 solidity , 數值運算與一般程式語言一樣:

  • 加法: x+y
  • 減法: x-y
  • 乘法: x*y
  • 除法: x/y
  • 取餘數: x%y (e.g., 13 % 5 = 3)

另外也支援指數運算

uint x = 5**2; // x = 5*5 = 25

為了保證 Zombie 的 DNA 字元只有 16 個字元長

所以建立一個 uint 變數 dnaModulus = dnaDigits的16次方

這樣每次只要把數值 % dnaModulus 就會滿足這個條件

更新 ZombieFactory 如下

pramga solidity >=0.5.0 <0.6.0;

contract ZombieFactory {
    uint dnaDigits = 16;
    uint dnaModulus = dnaDigits**16;
}

Structs 資料結構

在 solidity 除了基礎的資料結構外,也支援複合型資料結構

透過 struct 來做宣告

舉例如下:

struct Person {
    uint age;
    string name;
}

其中 string 是儲存 UTF-8 編碼的字串,也就是每個字元只有 8-bit。

因為每個 Zombie 需要有屬於自己的資料,也就是 dna 跟 name

所以需要建立一個 struct 名稱設定為 Zombie

Zombie 具有兩個屬性:

  1. name: 字串代表其名字
  2. dna: uint 代表其特徵值

更新 ZombieFactory 如下

pramga solidity >=0.5.0 <0.6.0;

contract ZombieFactory {
    uint dnaDigits = 16;
    uint dnaModulus = dnaDigits**16;
    
    struct Zombie {
        string name;
        uint dna;
    }
}

Arrays

當要存儲大量相同類型的資料時

就可以使用 array

在 Solidity ,根據陣列長度是否為固定分成 fixed 與 dynamic

// fixed array 資料集長度固定
uint[2] fixedArray;
// dynamic array 資料集長度不固定
uint[] dynamicArray;

Public 屬性

狀態資料可以把其存取屬性設定為 public , solidity 會自動替這個屬性產生 getter,讓其他有用到的地方讀取。

語法如下

Person[] public people;

在 ZombieFactory ,需要使用一個 dynamic Array 來紀錄所有產生出來的 Zombie 物件,並且需要讓這個 dynamic Array 存取權限是 public。

更新 ZombieFactory 如下

pramga solidity >=0.5.0 <0.6.0;

contract ZombieFactory {
    uint dnaDigits = 16;
    uint dnaModulus = dnaDigits**16;
    
    struct Zombie {
        string name;
        uint dna;
    }
    Zombie[] public zombies;
}

函式宣告

函式宣告語法如下

function eatHamburgers(string memory _name, uint _amount) public {}

函式宣告包含元素如下:

  1. 函式名稱: 如上面函式名稱是 eatHamburgers

  2. 參數: 參數根據其傳遞方式分為兩種。 pass by value:傳值,只會把值做複製傳入,在函式中修改不影響其原本的值。 pass by reference:傳參考,會把該參數的參考指標傳入,在函式中修改其值,會透過參考指標改到原本的值,對於參考值需要加入 memory 在參數前面。而參考類型比如 string, structs, mapping, 還有 arrays都需要。

  3. 存取權限:這邊的 eatHamburgers 是 public,代表可以直接從 abi 呼叫這個 function

特別注意的是,習慣上的命名規則會把傳入參數以底線當作開頭。

ZombieFactory 需要加入一個 public function 名稱叫作 createZombie。 createZombie 需要兩個參數一個是字串 _name,一個是 uint _dna

更新 ZombieFactory 如下

pramga solidity >=0.5.0 <0.6.0;

contract ZombieFactory {
    uint dnaDigits = 16;
    uint dnaModulus = dnaDigits**16;
    
    struct Zombie {
        string name;
        uint dna;
    }
    Zombie[] public zombies;
    
    function createZombie(string memory _name, uint _dna) public {
        
    }
}

使用 Struct and Arrays

在 solidity 中,

產生 struct 的語法如下:

struct Person {
    uint age;
    string name;
}
// 產生一個新的 Person
Person satoshi = Person(20, "Satoshi");

array 可以透過 array.push 新增元素到 array 之內

舉例如下

Person[] public people;
people.push(Person(20, "Satoshi"));

加入 createZombie 邏輯

createZombie 會產生一個新的 Zombie,並且把這個 Zombie 加入 zombies array。

更新 ZombieFactory 如下

pramga solidity >=0.5.0 <0.6.0;

contract ZombieFactory {
    uint dnaDigits = 16;
    uint dnaModulus = dnaDigits**16;
    
    struct Zombie {
        string name;
        uint dna;
    }
    Zombie[] public zombies;
    
    function createZombie(string memory _name, uint _dna) public {
        zombies.push(Zombie(_name, _dna));     
    }
}

public/ private function

solidity 對於 function, 有 public 與 private 權限。

在 solidity , function 預設都是 public。代表所有人都可以透過 abi 來呼叫 function 。

然而這樣會很容易把一些能夠修改資料的邏輯給透露給所有人,並非是一種安全的設計。

因此比較好的方式,把 function 預設設定為 private 限定只有 contract 物件可以呼叫。

只把一些需要外部互動的部份給設定成 public。

如下:

uint[] numbers;

function _addToArray(uint _number) private {
    numbers.push(_number);
}

通常習慣把 private 函式命名以底現作為開頭。

更新 createZombie 為 private

更新 ZombieFactory 如下

pramga solidity >=0.5.0 <0.6.0;

contract ZombieFactory {
    uint dnaDigits = 16;
    uint dnaModulus = dnaDigits**16;
    
    struct Zombie {
        string name;
        uint dna;
    }
    Zombie[] public zombies;
    
    function _createZombie(string memory _name, uint _dna) private {
        zombies.push(Zombie(_name, _dna));     
    }
}

function 回傳值與修飾子

function 回傳值

讓 function 具有回傳值的語法如下:

string greeting = "What's up dog";

function sayHello() public returns (string memory) {
    return greeting;
}
function 修飾子
  1. view: 代表該 function 只會讀取參考值,並不去做修改。

如下:

string greeting = "What's up dog";

function sayHello() public view returns (string memory) {
    return greeting;
}
  1. pure: 代表該 function 只做純量運算,不會去存取任何參考值。

如下:

function _multiply(uint _a, uint _b) private pure returns (uint) {
    return _a * _b;
}

建立 _generateRandomDna function

每個 Zombie 具有一個特徵值 dna

ZombieFactory 會需要一個產生隨機數的 function 來處理這個功能。

  1. 建立一個 private function 名稱設定為 _generateRandomDna。_generateRandomDna 具有一個字串參數 _str,並且具有一個 uint 回傳值。

  2. _generateRandomDna 不能修改 Contract 其他值,需要加入 view 修飾子。

更新 ZombieFactory 如下

pramga solidity >=0.5.0 <0.6.0;

contract ZombieFactory {
    uint dnaDigits = 16;
    uint dnaModulus = dnaDigits**16;
    
    struct Zombie {
        string name;
        uint dna;
    }
    Zombie[] public zombies;
    
    function _createZombie(string memory _name, uint _dna) private {
        zombies.push(Zombie(_name, _dna));     
    }
    
    function _generateRandomDna(string memory _str) private view returns(uint) {
        
    }
}

Keccak256 與型別轉換

Keccak256

Ethereum 內建 keccak256 函式是一個用來做雜湊值的函數,把輸入資料對應到 256-bit 的 16進位數值。

輸入必須是 bytes,所以需要把輸入字串透過 abi.encodedPacked轉換成 byte。如下:

//6e91ec6b618bb462a4a6ee5aa2cb0e9cf30f7a052bb467b0ba58b8748c00d2e5
keccak256(abi.encodePacked("aaaab"));
//b1f078126895a1424524de5321b339ab00408010b7cf0e6ed451514981e58aa9
keccak256(abi.encodePacked("aaaac"));
型別轉換

型別轉換是用來轉換兩種不同型別,語法如下

uint8 a = 5;
uint b = 6;
// throws an error because a * b returns a uint, not uint8:
uint8 c = a * b;
// we have to typecast b as a uint8 to make it work:
uint8 c = a * uint8(b);

更新 _generateRandomDna

  1. 透過 abi.encodePacked(_str) 把輸入 string 產生 256 bit hexicimal string 再透過行別轉換成 uint 並且存入一個 uint 變數 rand
  2. 把這個 rand % dnaModulus 在回傳

更新 ZombieFactory 如下

pramga solidity >=0.5.0 <0.6.0;

contract ZombieFactory {
    uint dnaDigits = 16;
    uint dnaModulus = dnaDigits**16;
    
    struct Zombie {
        string name;
        uint dna;
    }
    Zombie[] public zombies;
    
    function _createZombie(string memory _name, uint _dna) private {
        zombies.push(Zombie(_name, _dna));     
    }
    
    function _generateRandomDna(string memory _str) private view returns(uint) {
        uint rand = uint(keccak256(abi.encodePacked(_str)));
        return rand % dnaModulus;
    }
}

新增 createRandomZombie

  1. 建立一個 public function,設定名稱叫作 createRandomZombie 。 createRandomZombie 需要一個字串參數 _name。
  2. 把 _generateRandomDna 的值存在一個 uint 變數 randDna。
  3. 把 _name 跟 randDna 傳入 _createZombie 函數。

更新 ZombieFactory 如下

pramga solidity >=0.5.0 <0.6.0;

contract ZombieFactory {
    uint dnaDigits = 16;
    uint dnaModulus = dnaDigits**16;
    
    struct Zombie {
        string name;
        uint dna;
    }
    Zombie[] public zombies;
    
    function _createZombie(string memory _name, uint _dna) private {
        zombies.push(Zombie(_name, _dna));     
    }
    
    function _generateRandomDna(string memory _str) private view returns(uint) {
        uint rand = uint(keccak256(abi.encodePacked(_str)));
        return rand % dnaModulus;
    }
    
    function createRandomZombie(string memory _name) public {
        uint randDna = _generateRandomDna(_name);
        _createZombie(_name, randomDna);
    }
}

Events

Events 是一種讓 Contract 來根據 blockchain 資料變動來與前端應用互動的方式,透過這種方式可以監聽一些特定的變動。

如下:

// declare the event
event IntegersAdded(uint x, uint y, uint result);

function add(uint _x, uint _y) public returns (uint) {
  uint result = _x + _y;
  // fire an event to let the app know the function was called:
  emit IntegersAdded(_x, _y, result);
  return result;
}

前端應用就可以透過以下語法來監聽:

YourContract.IntegersAdded(function(error, result) {
  // do something with result
})

新增 NewZombie Event

  1. 建立一個 NewZombie Event,並且傳遞資料 zombieId(uint), name(string), 還有 dna(uint)。
  2. 新增驅動 NewZomie 到 _createZombie 邏輯裡,當新的 Zombie 物件加入 zombie 之後。
  3. 需要透過存儲 array.push 的回傳值到一個 uint 變數 id 用來當作 NewZombie 的參數。

更新 ZombieFactory 如下

pramga solidity >=0.5.0 <0.6.0;

contract ZombieFactory {
    event NewZombie(uint zombieId, string name, uint dna);
    uint dnaDigits = 16;
    uint dnaModulus = dnaDigits**16;
    
    struct Zombie {
        string name;
        uint dna;
    }
    Zombie[] public zombies;
    
    function _createZombie(string memory _name, uint _dna) private {
        uint id = zombies.push(Zombie(_name, _dna)) - 1;
        emit NewZombie(id, _name, _dna)
    }
    
    function _generateRandomDna(string memory _str) private view returns(uint) {
        uint rand = uint(keccak256(abi.encodePacked(_str)));
        return rand % dnaModulus;
    }
    
    function createRandomZombie(string memory _name) public {
        uint randDna = _generateRandomDna(_name);
        _createZombie(_name, randomDna);
    }
}

到這裡,完整的 ZombieFactory 邏輯完成了

當把整個 ZombieFactory Contract Deploy 到已太鏈上時,

我們可以透過產生出來的 abi 使用 web3.js 從前端去呼叫 contract 來產生隨機 Zombie。


上一篇
從以太坊白皮書理解 web 3 概念 - Day8
下一篇
從以太坊白皮書理解 web 3 概念 - Day10
系列文
從以太坊白皮書理解 web 3 概念32
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言